module common.imagearray;

import std.c.string : memcpy, memmove, memset;
import std.math, std.datetime, std.stream;
import std.algorithm;

import share.types;

import allegro5.allegro;


enum LibraryType { LoadMemory, LoadManual, UseCache }
enum InternationalVersion { Korean, Taiwan, Chinese, English }


const WilNone = 20020;
const WixNone = 20021;

const WilTaiwan = 20030;
const WixTaiwan = 20031;

const WilChinesse = 20040;
const WixChinesse = 20041;

const WilEnglish = 20050;
const WixEnglish = 20051;

const WilMagicCode = 0xC02a1173;
const WixMagicCode = 0xB13a11F0;

const MaxIndex = 100_000;
const MinImageSize = 2;
const MaxImageSize = 1920 * 1080; // HD 1080

align(1) struct ImageHeader
{
    align(1):
    public ushort Compressed;
    public char[20] Title; // 'WEMADE Entertainment inc.'
    public ushort Version;
    public int ImageCount;
}

align(1) public struct ImageInfo
{
    align(1):
    public short Width;
    public short Height;
    public short PosX;
    public short PosY;
    public ubyte Shadow;
    public short ShadowPX;
    public short ShadowPY;
    public uint ImageLength;
}

align(1) struct IndexHeader
{
    align(1):
    public char[20] Title;        //'WEMADE Entertainment inc.'
    public int IndexCount;
    public int Version;
}

private struct ImageRecord
{
    public ImageInfo Info;

    public ALLEGRO_BITMAP* Bitmap;
    public TickDuration LatestTime;
}



private void Bit16To24Bit(ushort color16, ref uint argb)
{
    argb = ((color16 & 0xF800) << 16) + ((color16 & 0x07E0) << 13) + ((color16 & 0x001F) << 11) + 0xFF;
    /*ubyte r = (color16 & 0xF800) >> 8;
        ubyte g = (color16 & 0x07E0) >> 3;
        ubyte b = (color16 & 0x001F) >> 3;
        ubyte a = 0xff;
        uint argb = ((a & 0xff) << 24 | (r & 0xff) << 16 | (g & 0xff) << 8 | (b & 0xff));

        return argb;*/
}

private void Bit16To24Bit(ushort color16, ref ALLEGRO_COLOR col)
{

    //uint argb = ((color16 & 0xF800) << 16) + ((color16 & 0x07E0) << 13) + ((color16 & 0x001F) << 11) + 0xFF;
    //return argb;
    ubyte r = (color16 & 0xF800) >> 8;
    ubyte g = (color16 & 0x07E0) >> 3;
    ubyte b = (color16 & 0x001F) >> 3;
    ubyte a = 0xff;
    col = al_map_rgba(r, g, b, a);
}

private ALLEGRO_BITMAP* CopyDataToBitmap32(int nWidth, int nHeight, in ushort[] pwData)
{
    if ((nWidth > 0) && (nHeight > 0))
    {
        auto bmp = al_create_bitmap(nWidth, nHeight);
        auto lr = al_lock_bitmap(bmp, ALLEGRO_PIXEL_FORMAT.ALLEGRO_PIXEL_FORMAT_RGBA_8888, ALLEGRO_LOCK_WRITEONLY);

        if (lr is null || lr.data is null /*|| lr.pitch <= 0*/)
            return null;
  
        auto pwdDst = cast(uint*)lr.data;
        if (lr.pitch < 0)
        {
            // OpenGL, no need
            //memset(pwdDst, 0, -lr.pitch * nHeight);
        }
        else
            memset(pwdDst, 0, lr.pitch * nHeight);
        auto rc = IntRectangle(0, 0, nWidth, nHeight);
        
        int nWidthStart = 0;
        int nWidthEnd = 0;
        int nCurrWidth = 0;
        int nCntCopyWord = 0;
        int nYCnt = 0;
        int nLastWidth = 0;
        int nCheck;
        
        ushort wPixel, wDyingKind;
        uint wColor;
        
        // yÃà Å¬¸®ÇÎ.
        for ( nYCnt = 0; nYCnt < rc.getTop(); nYCnt++ )
        {
            nWidthEnd += pwData[nWidthStart];
            nWidthStart++;
            nWidthEnd++;
            nWidthStart = nWidthEnd;
        }
        
        // yÃà¿¡ ´ëÇØ¼­ ½ÇÁ¦·Î ·çÇÎ½ÃÅ³ Count¸¸À» Á¤ÇÑ´Ù.
        for ( nYCnt = rc.getTop(); nYCnt < rc.getBottom(); nYCnt++ )
        {
            // ÇÑ¶óÀÎÀÇ ±æÀÌ¸¦ ¾ò¾î¿Â´Ù.(´ÜÀ§´Â ¿öµå)
            nWidthEnd += pwData[nWidthStart];
            nWidthStart++;
            
            // ¶óÀÎÀÇ ±æÀÌ¸¸Å­ È­¸é¿¡ »Ñ·ÁÁØ´Ù.
            for ( int x = nWidthStart; x < nWidthEnd; )
            {
                wDyingKind = pwData[x];
                if ( wDyingKind == 0xC0 )
                {
                    x++;
                    nCntCopyWord = pwData[x];
                    x++;
                    nCurrWidth += nCntCopyWord;
                }
                else if ( wDyingKind == 0xC1 || wDyingKind == 0xC2 || wDyingKind == 0xC3)
                {
                    x++;
                    nCntCopyWord = pwData[x];
                    x++;
                    
                    nLastWidth = nCurrWidth;
                    nCurrWidth += nCntCopyWord;
                    
                    if ( rc.getLeft() > nCurrWidth || rc.getRight() < nLastWidth )
                    {
                        x += nCntCopyWord;
                    }
                    else
                    {
                        // rc.leftÁ¡À» ±âÁØÀ¸·Î Ä«ÇÇÇÒ ¿µ¿ªÀÌ °É¸° °æ¿ì.
                        if ( nLastWidth < rc.getLeft() && rc.getLeft() <= nCurrWidth )
                        {
                            x += (rc.getLeft()-nLastWidth);
                            for (nCheck = 0; nCheck < (nCurrWidth-rc.getLeft()); nCheck++)
                            {
                                wPixel = pwData[x + nCheck];
                                Bit16To24Bit(wPixel, wColor);

                                uint* PtrL = cast(uint*)(cast(int)(pwdDst) + ((nYCnt) * lr.pitch) + (nLastWidth+nCheck) * 4);
                                *PtrL = wColor;
                            }
                            x += nCntCopyWord;//(nCurrWidth-rc.getLeft());
                        }
                        // rc.rightÁ¡À» ±âÁØÀ¸·Î Ä«ÇÇÇÒ ¿µ¿ªÀÌ °É¸° °æ¿ì.
                        else if ( nLastWidth <= rc.getRight() && rc.getRight() < nCurrWidth )
                        {
                            for (nCheck = 0; nCheck < (rc.getRight()-nLastWidth); nCheck++)
                            {
                                wPixel = pwData[x + nCheck];
                                Bit16To24Bit(wPixel, wColor);

                                uint* PtrL = cast(uint*)(cast(int)(pwdDst) + ((nYCnt) * lr.pitch) + (nLastWidth+nCheck) * 4);
                                *PtrL = wColor;
                            }
                            x += nCntCopyWord;
                        }
                        else
                        {
                            for (nCheck = 0; nCheck < nCntCopyWord; nCheck++)
                            {
                                wPixel = pwData[x + nCheck];
                                Bit16To24Bit(wPixel, wColor);

                                uint* PtrL = cast(uint*)(cast(int)(pwdDst) + ((nYCnt) * lr.pitch) + (nLastWidth+nCheck) * 4);
                                *PtrL = wColor;
                            }
                            x += nCntCopyWord;
                        }
                    }
                }
                else
                    break;
            }
            // ¶óÀÎÀÇ ³¡À» ´ÙÀ½ ¶óÀÎÀÇ ½ÃÀÛÀ¸·Î ¿Å°ÜÁØ´Ù.
            nWidthEnd++;
            
            nWidthStart = nWidthEnd;
            nCurrWidth = 0;
        }
        
        al_unlock_bitmap(bmp);
        return bmp;
    }
    
    return null;
}

private ALLEGRO_BITMAP* CopyDataToBitmap16(int nWidth, int nHeight, in ushort[] pwData)
{
    if ((nWidth > 0) && (nHeight > 0))
    {
        auto bmp = al_create_bitmap(nWidth, nHeight);
        auto lr = al_lock_bitmap(bmp, ALLEGRO_PIXEL_FORMAT.ALLEGRO_PIXEL_FORMAT_RGB_565, ALLEGRO_LOCK_WRITEONLY);

        if (lr is null || lr.data is null /*|| lr.pitch <= 0*/)
            return null;
        
        ushort* pwdDst = cast(ushort*)lr.data;
        if (lr.pitch < 0)
        {
            // OpenGL, no need
            //memset(pwdDst, 0, -lr.pitch * nHeight);
        }
        else
            memset(pwdDst, 0, lr.pitch * nHeight);
        IntRectangle rc = IntRectangle(0, 0, nWidth, nHeight);
        
        int nWidthStart = 0;
        int nWidthEnd = 0;
        int nCurrWidth = 0;
        int nCntCopyWord = 0;
        int nYCnt = 0;
        int nLastWidth = 0;
        int nCheck;
        
        ushort wPixel, wDyingKind;
        uint wColor;
        
        // yÃà Å¬¸®ÇÎ.
        for ( nYCnt = 0; nYCnt < rc.getTop(); nYCnt++ )
        {
            nWidthEnd += pwData[nWidthStart];
            nWidthStart++;
            nWidthEnd++;
            nWidthStart = nWidthEnd;
        }
        
        // yÃà¿¡ ´ëÇØ¼­ ½ÇÁ¦·Î ·çÇÎ½ÃÅ³ Count¸¸À» Á¤ÇÑ´Ù.
        for ( nYCnt = rc.getTop(); nYCnt < rc.getBottom(); nYCnt++ )
        {
            // ÇÑ¶óÀÎÀÇ ±æÀÌ¸¦ ¾ò¾î¿Â´Ù.(´ÜÀ§´Â ¿öµå)
            nWidthEnd += pwData[nWidthStart];
            nWidthStart++;
            
            // ¶óÀÎÀÇ ±æÀÌ¸¸Å­ È­¸é¿¡ »Ñ·ÁÁØ´Ù.
            for ( int x = nWidthStart; x < nWidthEnd; )
            {
                wDyingKind = pwData[x];
                if ( wDyingKind == 0xC0 )
                {
                    x++;
                    nCntCopyWord = pwData[x];
                    x++;
                    nCurrWidth += nCntCopyWord;
                }
                else if ( wDyingKind == 0xC1 || wDyingKind == 0xC2 || wDyingKind == 0xC3)
                {
                    x++;
                    nCntCopyWord = pwData[x];
                    x++;
                    
                    nLastWidth = nCurrWidth;
                    nCurrWidth += nCntCopyWord;
                    
                    if ( rc.getLeft() > nCurrWidth || rc.getRight() < nLastWidth )
                    {
                        x += nCntCopyWord;
                    }
                    else
                    {
                        // rc.leftÁ¡À» ±âÁØÀ¸·Î Ä«ÇÇÇÒ ¿µ¿ªÀÌ °É¸° °æ¿ì.
                        if ( nLastWidth < rc.getLeft() && rc.getLeft() <= nCurrWidth )
                        {
                            x += (rc.getLeft()-nLastWidth);
                            memcpy(&pwdDst[(nYCnt * (lr.pitch >> 1)) + rc.getLeft()], &pwData[x], ushort.sizeof*(nCurrWidth-rc.getLeft()));
                            x += (nCurrWidth-rc.getLeft());
                        }
                        // rc.rightÁ¡À» ±âÁØÀ¸·Î Ä«ÇÇÇÒ ¿µ¿ªÀÌ °É¸° °æ¿ì.
                        else if ( nLastWidth <= rc.getRight() && rc.getRight() < nCurrWidth )
                        {
                            memcpy(&pwdDst[(nYCnt * (lr.pitch >> 1)) + nLastWidth], &pwData[x], ushort.sizeof*(rc.getRight()-nLastWidth));
                            x += nCntCopyWord;
                        }
                        else
                        {
                            memcpy(&pwdDst[(nYCnt * (lr.pitch >> 1)) + nLastWidth], &pwData[x], ushort.sizeof*nCntCopyWord);
                            x += nCntCopyWord;
                        }
                    }
                }
                else
                    break;
            }
            // ¶óÀÎÀÇ ³¡À» ´ÙÀ½ ¶óÀÎÀÇ ½ÃÀÛÀ¸·Î ¿Å°ÜÁØ´Ù.
            nWidthEnd++;
            
            nWidthStart = nWidthEnd;
            nCurrWidth = 0;
        }
        
        al_unlock_bitmap(bmp);
        al_convert_bitmap(bmp);
 
        return bmp;
    }
    
    return null;
}


final class ImageArray
{
    private static ushort[] cache;

    private int WilVersion;
    private int WixVersion;
    private TickDuration MemCheckTime;
    private ImageRecord[] ImgArr;
    private uint[] IndexList;
    private File reader;

    public ImageInfo Info;


    public string FileName;
    public int ImageCount;
    public LibraryType LibType;
    public InternationalVersion IVersion;
    public int MaxMemorySize;

    
    
    public ALLEGRO_BITMAP* opIndex(size_t index)
    {
        return GetImage(index);
    }
    
    
    
    
    public this()
    {
        FileName = "";
        LibType = LibraryType.UseCache;
        ImageCount = 0;
        MaxMemorySize = 1024 * 1000; //1M
        cache = new ushort[MaxImageSize];
        
        reader = null;
        ImgArr = null;
        IndexList = null;
        Info = Info.init;
        
        MemCheckTime = Clock.currSystemTick();
        IVersion = InternationalVersion.English;
        WilVersion = WilNone;
        WixVersion = WixNone;
    }

    public ~this()
    {
        finalize();
    }
    
    /// <summary>
    /// Load all of the surfaces into memory
    /// </summary>
    private void LoadAllData()
    {
        int imglen;
        ImageInfo imgi;
        ImageRecord imgrd;
        
        for (int i = 0; i < ImageCount; i++)
        {
            reader.readBlock(&imgi, imgi.sizeof);

            imglen = imgi.ImageLength;
            //imglen = imgi.Width * imgi.Height;
            if (imglen < MinImageSize || imglen > MaxImageSize)
                break;
            
            reader.readBlock(&cache[0], imglen * ushort.sizeof);
            imglen = imgi.Width * imgi.Height;
            
            imgrd.Info = imgi;
            imgrd.Bitmap = CopyDataToBitmap32(imgi.Width, imgi.Height, cache[0..imglen]);
            
            ImgArr[i] = imgrd;
        }
    }
    
    /// <summary>
    /// Load the index file which contains the locations of the individual
    /// images in the .WIL file.
    /// </summary>
    /// <param name="idxfile">The filename which shall be loaded.</param>
    private bool LoadIndexFile(string idxfile)
    {
        IndexList.length = 0;
    
        if (std.file.exists(idxfile) == false)
            return false;
            
        File idxreader = new File;
        scope(exit) { destroy(idxreader); }
        
        idxreader.open(idxfile, FileMode.In);
        if ( idxreader.isOpen() )
        {
            size_t readsz;
            if (idxreader.size < IndexHeader.sizeof)
                return false;

            IndexHeader header;
            readsz = idxreader.readBlock(&header, header.sizeof);

            if (header.IndexCount < 0 || header.IndexCount > MaxIndex)
                throw new Exception("Image index library has invalid count.");
            
            IndexList.length = header.IndexCount;
            readsz = idxreader.readBlock(&IndexList[0], 4 * header.IndexCount);
            
            idxreader.close();
            
            return true;
        }
        
        return false;
    }
    
    /// <summary>
    /// Load a single image from the file and create a Allegro texture.
    /// </summary>
    /// <param name="position">The position of the image to load.</param>
    /// <param name="dximg">The place to store the texture.</param>
    private void loadImage(uint position, out ImageRecord imgrd)
    {
        if (position == 0)
            return;

        reader.seek(position, SeekPos.Set);
        
        int imglen;
        ImageInfo imgi;
        reader.readBlock(&imgi, imgi.sizeof);

        imglen = imgi.ImageLength;
        //imglen = imgi.Width * imgi.Height;
        if (imglen < MinImageSize || imglen > MaxImageSize)
            return;

        reader.readBlock(&cache[0], imglen * ushort.sizeof);

        Info = imgi;
        imgrd.Info = imgi;
        imgrd.Bitmap = CopyDataToBitmap32(imgi.Width, imgi.Height, cache[0..imglen]);
    }

    private void loadMemory(uint position, out ushort[] buffer)
    {
        if (position == 0)
            return;

        reader.seek(position, SeekPos.Set);

        ImageInfo imgi;
        reader.readBlock(&imgi, imgi.sizeof);
        
        auto imglen = imgi.ImageLength;
        //auto imglen = imgi.Width * imgi.Height;
        if (imglen < MinImageSize || imglen > MaxImageSize)
            return;
        
        reader.readBlock(&cache[0], imglen * ushort.sizeof);

        Info = imgi;
        buffer = cache[0..imglen];
    }

    private void FreeOldMemorys()
    {
        int n = 0;
        auto curtime = Clock.currSystemTick();
        for (int i = 0; i < ImageCount; i++)
        {
            if (ImgArr[i].Bitmap !is null)
            {
                if ((curtime - ImgArr[i].LatestTime).msecs > 2 * 60 * 1000)
                {
                    al_destroy_bitmap(ImgArr[i].Bitmap);
                    ImgArr[i].Bitmap = null;
                    n++;
                }
            }
        }
    }
    
    private ALLEGRO_BITMAP* GetImage(int index)
    {
        switch (LibType)
        {
            case LibraryType.UseCache:
                return GetCachedImage(index);
            case LibraryType.LoadMemory:
                if ((index >= 0) && (index < ImageCount))
                    return ImgArr[index].Bitmap;
                break;
            default:
                return null;
        }
        
        return null;
    }
    
    /// <summary>
    /// This will make the class ready for use.
    /// </summary>
    public void initialize()
    {
        if (FileName == "")
            throw new Exception("FileName not assigned.");

        if (std.file.exists(FileName) == false)
            return;
            
        if (reader is null)
            reader = new File(FileName, FileMode.In);
        //reader.open(FileName, FileMode.In);
        if (reader.size < ImageHeader.sizeof)
            throw new Exception("This is not a valid image library.");
        
        ImageHeader header;
        reader.readBlock(&header, header.sizeof);

        final switch (IVersion)
        {
            case InternationalVersion.Korean:
                {
                    WilVersion = WilTaiwan;
                    WixVersion = WixTaiwan;
                    break;
                }
            case InternationalVersion.Taiwan:
                {
                    WilVersion = WilTaiwan;
                    WixVersion = WixTaiwan;
                    break;
                }
            case InternationalVersion.Chinese:
                {
                    WilVersion = WilChinesse;
                    WixVersion = WixChinesse;
                    break;
                }
            case InternationalVersion.English:
                {
                    WilVersion = WilEnglish;
                    WixVersion = WixEnglish;
                    break;
                }
        }

        ImageCount = header.ImageCount;

        if (ImageCount <= 0 || ImageCount >= MaxIndex)
            throw new Exception("Image library has invalid count.");
        ImgArr = new ImageRecord[ImageCount];
        
        auto idxfile = std.path.setExtension(FileName, ".WIX");

        if (LibType == LibraryType.LoadMemory)
            LoadAllData();
        else
            LoadIndexFile(idxfile);
    }
    
    /// <summary>
    /// We should call this when we are finished with the class.
    /// </summary>
    public void finalize()
    {
        clearCache();
        
        if (ImgArr !is null)
            destroy(ImgArr);
        ImgArr = null;
        
        if (reader !is null)
            reader.close();
        reader = null;
    }
    
    public void clearCache()
    {
        if (ImgArr !is null)
        {
            for (int i = 0; i < ImageCount; i++)
            {
                if (ImgArr[i].Bitmap !is null)
                {
                    al_destroy_bitmap(ImgArr[i].Bitmap);
                    ImgArr[i].Bitmap = null;
                }
            }
            //GC.Collect();
        }
    }

    public ushort[] GetMemory(int index)
    {
        ushort[] buffer = void;

        if ((index < 0) || (index >= ImageCount))
            return null;

        if (index < IndexList.length)
        {
            auto position = IndexList[index];
            loadMemory(position, buffer);
            return buffer;
        }

        return null;
    }

    public ALLEGRO_BITMAP* GetCachedImage(int index)
    {
        if ((index < 0) || (index >= ImageCount))
            return null;
        
        auto curtime = Clock.currSystemTick();
        
        if ((curtime - MemCheckTime).msecs > 10000)
        {
            MemCheckTime = curtime;
            FreeOldMemorys();
        }
        if (ImgArr[index].Bitmap is null) //cacheµÇ¾î ÀÖÁö ¾ÊÀ½. »õ·Î ÀÐ¾î¾ßÇÔ.
        {
            if (index < IndexList.length)
            {
                auto position = IndexList[index];
                loadImage(position, ImgArr[index]);
                ImgArr[index].LatestTime = curtime;
                return ImgArr[index].Bitmap;
            }
        }
        else
        {
            ImgArr[index].LatestTime = curtime;
            return ImgArr[index].Bitmap;
        }
        
        return null;
    }

    public ALLEGRO_BITMAP* GetCachedImageEx(int index, ref ImageInfo imgi)
    {
        if ((index < 0) || (index >= ImageCount))
            return null;
        
        auto curtime = Clock.currSystemTick();
        
        if ((curtime - MemCheckTime).msecs > 10000)
        {
            MemCheckTime = curtime;
            FreeOldMemorys();
        }
        if (ImgArr[index].Bitmap is null) //cacheµÇ¾î ÀÖÁö ¾ÊÀ½. »õ·Î ÀÐ¾î¾ßÇÔ.
        {
            if (index < IndexList.length)
            {
                auto position = IndexList[index];
                loadImage(position, ImgArr[index]);
                ImgArr[index].LatestTime = curtime;
                imgi = ImgArr[index].Info;
                return ImgArr[index].Bitmap;
            }
        }
        else
        {
            ImgArr[index].LatestTime = curtime;
            imgi = ImgArr[index].Info;
            return ImgArr[index].Bitmap;
        }
        
        return null;
    }
}